package com.htd.erp.web;

import java.util.ArrayList;
import java.util.List;

import javax.faces.context.FacesContext;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;

import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.ExtendedDataModel;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;
import org.apache.commons.lang3.StringUtils;
import org.richfaces.component.SortOrder;
import org.richfaces.model.Arrangeable;
import org.richfaces.model.ArrangeableState;
import org.richfaces.model.FilterField;
import org.richfaces.model.SortField;

import com.htd.erp.model.AbstractEntity;

public class JpaDataModel<T extends AbstractEntity> extends ExtendedDataModel<T> implements Arrangeable {
	
    private Object rowKey;
    private ArrangeableState arrangeableState;
    private final EntityManager entityManager;
    private final Class<T> entityClass;
    
	public JpaDataModel(Class<T> entityClass, EntityManager entityManager) {
		this.entityClass = entityClass;
		this.entityManager = entityManager;
	}

	@Override
	public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) {
		CriteriaQuery<T> criteriaQuery = createSelectCriteriaQuery();
        TypedQuery<T> query = getEntityManager().createQuery(criteriaQuery);
 
        SequenceRange sequenceRange = (SequenceRange) range;
        if (sequenceRange.getFirstRow() >= 0 && sequenceRange.getRows() > 0) {
            query.setFirstResult(sequenceRange.getFirstRow());
            query.setMaxResults(sequenceRange.getRows());
        }
 
        List<T> data = query.getResultList();
        for (T t : data) {
            visitor.process(context, t.getId(), argument);
        }
	}
	
	@Override
	public void arrange(FacesContext context, ArrangeableState arrangeableState) {
		this.arrangeableState = arrangeableState;
	}

	@Override
	public Object getRowKey() {
		return rowKey;
	}

	@Override
	public void setRowKey(Object rowKey) {
		this.rowKey = rowKey;
	}

	@Override
	public int getRowCount() {
		CriteriaQuery<Long> criteriaQuery = createCountCriteriaQuery();
        return getEntityManager().createQuery(criteriaQuery).getSingleResult().intValue();
	}

	@Override
	public T getRowData() {
		return getEntityManager().find(getEntityClass(), rowKey);
	}

	@Override
	public int getRowIndex() {
		return -1;
	}

	@Override
	public Object getWrappedData() {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean isRowAvailable() {
		return rowKey != null;
	}

	@Override
	public void setRowIndex(int arg0) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void setWrappedData(Object arg0) {
		throw new UnsupportedOperationException();
	}
	
	protected CriteriaQuery<T> createSelectCriteriaQuery() {
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(getEntityClass());
        Root<T> root = criteriaQuery.from(getEntityClass());
 
        if (arrangeableState != null) {
            List<Order> orders = createOrders(criteriaBuilder, root);
            if (!orders.isEmpty()) {
                criteriaQuery.orderBy(orders);
            }
            Expression<Boolean> filterCriteria = createFilterCriteria(criteriaBuilder, root);
            criteriaQuery.where(filterCriteria);
        }
 
        return criteriaQuery;
    }
 
    protected List<Order> createOrders(CriteriaBuilder criteriaBuilder, Root<T> root) {
        List<Order> orders = new ArrayList<Order>();
        List<SortField> sortFields = arrangeableState.getSortFields();
        if (sortFields != null && !sortFields.isEmpty()) {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            for (SortField sortField : sortFields) {
                String propertyName = (String) sortField.getSortBy().getValue(facesContext.getELContext());
                Path<Object> expression = root.get(propertyName);
                Order jpaOrder;
                SortOrder sortOrder = sortField.getSortOrder();
                if (sortOrder == SortOrder.ascending) {
                    jpaOrder = criteriaBuilder.asc(expression);
                } else if (sortOrder == SortOrder.descending) {
                    jpaOrder = criteriaBuilder.desc(expression);
                } else {
                    throw new IllegalArgumentException(sortOrder.toString());
                }
                orders.add(jpaOrder);
            }
        }
 
        return orders;
    }
	
	protected CriteriaQuery<Long> createCountCriteriaQuery() {
		CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
        Root<T> root = criteriaQuery.from(getEntityClass());
        Expression<Boolean> filterCriteria = createFilterCriteria(criteriaBuilder, root);
        if (filterCriteria != null) {
            criteriaQuery.where(filterCriteria);
        }
        Expression<Long> count = criteriaBuilder.count(root);
        criteriaQuery.select(count);
        return criteriaQuery;
	}
	
	protected Expression<Boolean> createFilterCriteria(CriteriaBuilder criteriaBuilder, Root<T> root) {
        Expression<Boolean> filterCriteria = null;
        List<FilterField> filterFields = arrangeableState.getFilterFields();
        if (filterFields != null && !filterFields.isEmpty()) {
            FacesContext facesContext = FacesContext.getCurrentInstance();
 
            for (FilterField filterField : filterFields) {
                String propertyName = (String) filterField.getFilterExpression().getValue(facesContext.getELContext());
                Object filterValue = filterField.getFilterValue();
 
                Expression<Boolean> predicate = createFilterCriteriaForField(propertyName, filterValue, root, criteriaBuilder);
 
                if (predicate == null) {
                    continue;
                }
                filterCriteria = filterCriteria == null ? predicate.as(Boolean.class) : criteriaBuilder.and(filterCriteria, predicate.as(Boolean.class));
            }
        }
        Expression<Boolean> notDeletedCriteria = createFilterCriteriaForField("deleted", "false", root, criteriaBuilder);
        return filterCriteria == null ? notDeletedCriteria : criteriaBuilder.and(notDeletedCriteria, filterCriteria);
    }
	
	protected Expression<Boolean> createFilterCriteriaForField(String propertyName, Object filterValue, Root<T> root,
            CriteriaBuilder criteriaBuilder) {
        String stringFilterValue = (String) filterValue;
        if (StringUtils.isBlank(stringFilterValue)) {
            return null;
        }
        stringFilterValue = stringFilterValue.toLowerCase(arrangeableState.getLocale());
        Path<String> expression = root.get(propertyName);
        Expression<Integer> locator = criteriaBuilder.locate(criteriaBuilder.lower(expression), stringFilterValue, 1);
        return criteriaBuilder.gt(locator, 0);
    }
	
	protected EntityManager getEntityManager() {
		return entityManager;
	}
	
	protected Class<T> getEntityClass() {
		return entityClass;
	}
}
